package com.wyz.socket.core.utils.buffer;
import java.io.*;


public class CircularByteBuffer {
    /**
     * 循环字节缓冲区的默认大小。
     */
    private final static int DEFAULT_SIZE = 1024;

    /**
     * 随着事物的增加，缓冲区将增加。
     */
    public final static int INFINITE_SIZE = -1;


    protected byte[] buffer;

    protected volatile int readPosition = 0;

    protected volatile int writePosition = 0;
    /**
     * 第一个保存字节的索引。 （支持流标记。）
     */
    protected volatile int markPosition = 0;
    /**
     *  必须保存的字节数 在InputStream上支持mark（）和reset（）。
     */
    protected volatile int markSize = 0;
    /**
     * If this buffer is infinite (should resize itself when full)
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile boolean infinite = false;
    /**
     * True if a write to a full buffer should block until the buffer
     * has room, false if the write method should throw an IOException
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean blockingWrite = true;
    /**
     * The InputStream that can empty this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected InputStream in = new CircularByteBufferInputStream();
    /**
     * true if the close() method has been called on the InputStream
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean inputStreamClosed = false;
    /**
     * The OutputStream that can fill this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected OutputStream out = new CircularByteBufferOutputStream();
    /**
     * true if the close() method has been called on the OutputStream
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean outputStreamClosed = false;

    /**
     * Make this buffer ready for reuse.  The contents of the buffer
     * will be cleared and the streams associated with this buffer
     * will be reopened if they had been closed.
     *
     * @since ostermillerutils 1.00.00
     */
    public void clear(){
        synchronized (this){
            readPosition = 0;
            writePosition = 0;
            markPosition = 0;
            outputStreamClosed = false;
            inputStreamClosed = false;
        }
    }

    /**
     * Retrieve a OutputStream that can be used to fill
     * this buffer.
     * <p>
     * Write methods may throw a BufferOverflowException if
     * the buffer is not large enough.  A large enough buffer
     * size must be chosen so that this does not happen or
     * the caller must be prepared to catch the exception and
     * try again once part of the buffer has been consumed.
     *
     *
     * @return the producer for this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    public OutputStream getOutputStream(){
        return out;
    }

    /**
     * Retrieve a InputStream that can be used to empty
     * this buffer.
     * <p>
     * This InputStream supports marks at the expense
     * of the buffer size.
     *
     * @return the consumer for this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    public InputStream getInputStream(){
        return in;
    }

    /**
     * 获取可读取的字节数。
     */
    public int getAvailable(){
        synchronized (this){
            return available();
        }
    }

    /**
     * 获取此缓冲区可写的空闲字节数。
     */
    public int getSpaceLeft(){
        synchronized (this){
            return spaceLeft();
        }
    }

    public int getSize(){
        synchronized (this){
            return buffer.length;
        }
    }

    /**
     * double the size of the buffer
     */
    private void resize(){
        byte[] newBuffer = new byte[buffer.length * 2];
        int marked = marked();
        int available = available();
        if (markPosition <= writePosition){
            // any space between the mark and
            // the first write needs to be saved.
            // In this case it is all in one piece.
            int length = writePosition - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length);
        } else {
            int length1 = buffer.length - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length1);
            int length2 = writePosition;
            System.arraycopy(buffer, 0, newBuffer, length1, length2);
        }
        buffer = newBuffer;
        markPosition = 0;
        readPosition = marked;
        writePosition = marked + available;
    }

    /**
     * Space available in the buffer which can be written.
     */
    private int spaceLeft(){
        if (writePosition < markPosition){
            return (markPosition - writePosition - 1);
        }
        return ((buffer.length - 1) - (writePosition - markPosition));
    }

    /**
     * Bytes available for reading.
     */
    private int available(){
        if (readPosition <= writePosition){
            return (writePosition - readPosition);
        }
        return (buffer.length - (readPosition - writePosition));
    }

    /**
     * Bytes saved for supporting marks.
     */
    private int marked(){
        if (markPosition <= readPosition){
            return (readPosition - markPosition);
        }
        return (buffer.length - (markPosition - readPosition));
    }

    /**
     * 如果我们通过了markSize，请重置
     *       标记，以便可以使用空间。
     */
    private void ensureMark(){
        if (marked() > markSize){
            markPosition = readPosition;
            markSize = 0;
        }
    }


    public CircularByteBuffer(){
        this (DEFAULT_SIZE, true);
    }

    public CircularByteBuffer(int size){
        this (size, true);
    }

    public CircularByteBuffer(boolean blockingWrite){
        this (DEFAULT_SIZE, blockingWrite);
    }

    public CircularByteBuffer(int size, boolean blockingWrite){
        if (size == INFINITE_SIZE){
            buffer = new byte[DEFAULT_SIZE];
            infinite = true;
        } else {
            buffer = new byte[size];
            infinite = false;
        }
        this.blockingWrite = blockingWrite;
    }

    protected class CircularByteBufferInputStream extends InputStream {

        @Override
        public int available() throws IOException {
            synchronized (CircularByteBuffer.this){
                if (inputStreamClosed) throw new IOException("InputStream has been closed, it is not ready.");
                return (CircularByteBuffer.this.available());
            }
        }

        @Override
        public void close() throws IOException {
            synchronized (CircularByteBuffer.this){
                inputStreamClosed = true;
            }
        }

        @Override
        public void mark(int readAheadLimit) {
            synchronized (CircularByteBuffer.this){
                //if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot mark a closed InputStream.");
                if (buffer.length - 1 > readAheadLimit) {
                    markSize = readAheadLimit;
                    markPosition = readPosition;
                }
            }
        }

        /**
         * Tell whether this stream supports the mark() operation.
         */
        @Override public boolean markSupported() {
            return true;
        }

        /**
         * Read a single byte.
         * This method will block until a byte is available, an I/O error occurs,
         * or the end of the stream is reached.
         */
        @Override public int read() throws IOException {
            while (true){
                synchronized (CircularByteBuffer.this){
                    if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot read from a closed InputStream.");
                    int available = CircularByteBuffer.this.available();
                    if (available > 0){
                        int result = buffer[readPosition] & 0xff;
                        readPosition++;
                        if (readPosition == buffer.length){
                            readPosition = 0;
                        }
                        ensureMark();
                        return result;
                    } else if (outputStreamClosed){
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }

        /**
         * Read bytes into an array.
         * This method will block until some input is available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param cbuf Destination buffer.
         * @return The number of bytes read, or -1 if the end of
         *   the stream has been reached
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public int read(byte[] cbuf) throws IOException {
            return read(cbuf, 0, cbuf.length);
        }

        /**
         * Read bytes into a portion of an array.
         * This method will block until some input is available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param cbuf Destination buffer.
         * @param off Offset at which to start storing bytes.
         * @param len Maximum number of bytes to read.
         * @return The number of bytes read, or -1 if the end of
         *   the stream has been reached
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public int read(byte[] cbuf, int off, int len) throws IOException {
            while (true){
                synchronized (CircularByteBuffer.this){
                    if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot read from a closed InputStream.");
                    int available = CircularByteBuffer.this.available();
                    if (available > 0){
                        int length = Math.min(len, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        System.arraycopy(buffer, readPosition, cbuf, off, firstLen);
                        if (secondLen > 0){
                            System.arraycopy(buffer, 0, cbuf, off+firstLen,  secondLen);
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (outputStreamClosed){
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }

        /**
         * Reset the stream.
         * If the stream has been marked, then attempt to reposition i
         * at the mark. If the stream has not been marked, or more bytes
         * than the readAheadLimit have been read, this method has no effect.
         *
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void reset() throws IOException {
            synchronized (CircularByteBuffer.this){
                if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot reset a closed InputStream.");
                readPosition = markPosition;
            }
        }

        /**
         * Skip bytes.
         * This method will block until some bytes are available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param n The number of bytes to skip
         * @return The number of bytes actually skipped
         * @throws IllegalArgumentException if n is negative.
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public long skip(long n) throws IOException, IllegalArgumentException {
            while (true){
                synchronized (CircularByteBuffer.this){
                    if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot skip bytes on a closed InputStream.");
                    int available = CircularByteBuffer.this.available();
                    if (available > 0){
                        int length = Math.min((int)n, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        if (secondLen > 0){
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (outputStreamClosed){
                        return 0;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }
    }

    /**
     * Class for writing to a circular byte buffer.
     * If the buffer is full, the writes will either block
     * until there is some space available or throw an IOException
     * based on the CircularByteBuffer's preference.
     *
     * @since ostermillerutils 1.00.00
     */
    protected class CircularByteBufferOutputStream extends OutputStream {

        /**
         * Close the stream, flushing it first.
         * This will cause the InputStream associated with this circular buffer
         * to read its last bytes once it empties the buffer.
         * Once a stream has been closed, further write() or flush() invocations
         * will cause an IOException to be thrown. Closing a previously-closed stream,
         * however, has no effect.
         *
         * @throws IOException never.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void close() throws IOException {
            synchronized (CircularByteBuffer.this){
                if (!outputStreamClosed){
                    flush();
                }
                outputStreamClosed = true;
            }
        }

        /**
         * Flush the stream.
         *
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void flush() throws IOException {
            synchronized (CircularByteBuffer.this){
                if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot flush a closed OutputStream.");
                if (inputStreamClosed) throw new IOException("Buffer closed by inputStream; cannot flush.");
            }
            // this method needs to do nothing
        }

        /**
         * Write an array of bytes.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param cbuf Array of bytes to be written
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void write(byte[] cbuf) throws IOException {
            write(cbuf, 0, cbuf.length);
        }

        /**
         * Write a portion of an array of bytes.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param cbuf Array of bytes
         * @param off Offset from which to start writing bytes
         * @param len - Number of bytes to write
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void write(byte[] cbuf, int off, int len) throws IOException {
            while (len > 0){
                synchronized (CircularByteBuffer.this){
                    if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream.");
                    if (inputStreamClosed) throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer.");
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < len){
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < len) throw new BufferOverflowException("CircularByteBuffer is full; cannot write " + len + " bytes");
                    int realLen = Math.min(len, spaceLeft);
                    int firstLen = Math.min(realLen, buffer.length - writePosition);
                    int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1);
                    int written = firstLen + secondLen;
                    if (firstLen > 0){
                        System.arraycopy(cbuf, off, buffer, writePosition, firstLen);
                    }
                    if (secondLen > 0){
                        System.arraycopy(cbuf, off+firstLen, buffer, 0, secondLen);
                        writePosition = secondLen;
                    } else {
                        writePosition += written;
                    }
                    if (writePosition == buffer.length) {
                        writePosition = 0;
                    }
                    off += written;
                    len -= written;
                }
                if (len > 0){
                    try {
                        Thread.sleep(100);
                    } catch(Exception x){
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }

        /**
         * Write a single byte.
         * The byte to be written is contained in the 8 low-order bits of the
         * given integer value; the 24 high-order bits are ignored.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param c number of bytes to be written
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override public void write(int c) throws IOException {
            boolean written = false;
            while (!written){
                synchronized (CircularByteBuffer.this){
                    if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream.");
                    if (inputStreamClosed) throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer.");
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < 1){
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < 1) throw new BufferOverflowException("CircularByteBuffer is full; cannot write 1 byte");
                    if (spaceLeft > 0){
                        buffer[writePosition] = (byte)(c & 0xff);
                        writePosition++;
                        if (writePosition == buffer.length) {
                            writePosition = 0;
                        }
                        written = true;
                    }
                }
                if (!written){
                    try {
                        Thread.sleep(100);
                    } catch(Exception x){
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }
    }
}
